package com.hzw.saas.web.hbox.service;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.hzw.saas.api.storage.ICatalogService;
import com.hzw.saas.api.storage.IResourceFileService;
import com.hzw.saas.api.storage.bo.ResourceFileStorageBO;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.storage.query.ResourceFileUpdateQuery;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.storage.enums.LocChunkStatusEnum;
import com.hzw.saas.common.storage.enums.StorageModeEnum;
import com.hzw.saas.common.storage.pojo.FileStorage;
import com.hzw.saas.common.storage.pojo.LocChunkStorage;
import com.hzw.saas.common.storage.service.IChunkStorageService;
import com.hzw.saas.common.storage.service.IFileStorageService;
import com.hzw.saas.common.util.Json;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.SnowFlakeUtil;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.web.hbox.bo.LocChunkBO;
import com.hzw.saas.web.hbox.util.HBoxUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestAlgorithm;
import cn.hutool.json.JSONUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * @author zzl
 * @since 03/12/2021
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class LocChunkServiceImpl implements ILocChunkService {

    private final PlatformTransactionManager transactionManager;

    private final ICatalogService catalogService;
    private final IResourceFileService fileService;
    private final IFileStorageService fileStorageService;
    private final IChunkStorageService chunkStorageService;

    @Override
    public boolean exist(String fileId, String chunkCatalog, long startBytes, long endBytes, String digest) {
        LocChunkBO locChunkBO = this.getChunk(fileId, chunkCatalog, startBytes, endBytes, digest);
        return Objects.nonNull(locChunkBO);
    }

    @Override
    public LocChunkBO getChunk(String fileId, String chunkCatalog, long startBytes, long endBytes, String digest) {
        List<LocChunkBO> chunkInfoList = this.listChunks(fileId, chunkCatalog);
        return chunkInfoList.stream().filter(chunkInfo ->
            Objects.equals(chunkInfo.getStartBytes(), startBytes)
                && Objects.equals(chunkInfo.getEndBytes(), endBytes)
                && (StringUtils.isBlank(digest) || Objects.equals(chunkInfo.getDigest(), digest)))
            .findAny().orElse(null);
    }

    @Override
    public List<LocChunkBO> listChunks(String fileId, String chunkCatalog) {
        if (StringUtils.isBlank(fileId))
            return new ArrayList<>();
        ResourceFileStorageBO file = fileService.getFileWithStorage(fileId, LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()), LogicQuery.none(), AssertEnum.FOUND);
        // 文件合并时无法获取分片列表
        AssertUtil.assertThrow("文件正在合并...", Objects.equals(FileIntegrityEnum.MERGING.getCode(), file.getIntegrity()));
        return this.listChunks(file, chunkCatalog, null);
    }

    /**
     * 分片保存需进行以下校验
     * 1. 必要参数
     * 2. 文件信息存在且状态正常
     * 3. 分片是否已上传/存在，判断标准为起始字节和文件摘要(可选)
     */
    @Override
    public LocChunkBO saveChunk(String chunkCatalog, LocChunkBO chunk, byte[] chunkData) {
        this.checkSaveChunkParam(chunk, chunkData);
        // 获取文件信息,检查文件存在性
        ResourceFileStorageBO fileInfo = fileService.getFileWithStorage(chunk.getFileId(),
            LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()), LogicQuery.none(), AssertEnum.FOUND);
        AssertUtil.assertThrow("文件正在合并...", Objects.equals(FileIntegrityEnum.MERGING.getCode(), fileInfo.getIntegrity()));
        // 检查分片是否上传过
        LocChunkBO chunkExist = this.getChunk(chunk.getFileId(), chunkCatalog,
            chunk.getStartBytes(), chunk.getEndBytes(), chunk.getDigest());
        if (Objects.nonNull(chunkExist)) {
            log.debug("分片已存在：{}", JSONUtil.toJsonStr(chunkExist));
            return chunkExist;
        }
        // 分片信息
        log.debug("接收上传分片：{}", Json.toJsonString(chunk));
        // 保存分片文件
        LocChunkStorage locChunkStorage = MapperUtil.nf().map(chunk, LocChunkStorage.class);
        locChunkStorage.setSuperPath(catalogService.getCacheSupPath(chunkCatalog, chunk.getFileId()));
        locChunkStorage.setLength(chunk.getEndBytes() - chunk.getStartBytes() + 1);
        chunkStorageService.saveChunk(locChunkStorage, chunkData);
        // 更新持久层信息
        List<LocChunkBO> chunks = HBoxUtil.decode(fileInfo.getChunkInfo());
        chunks.add(chunk);
        String chunksInfo = HBoxUtil.encodeChunkInfo(chunks);
        this.updateChunksInfo(chunk.getFileId(), null, chunksInfo);
        return chunk;
    }

    /**
     * 分片合并需校验
     * 1. 待合并分片信息不为空
     * 2. 文件信息存在且状态正常
     * 3. 文件完整性状态不为“合并中”
     * 4. 文件已上传分片信息不为空
     * 5. 待合并分片都为该文件已上传且能获取的的分片
     *
     * @param fileId      文件ID
     * @param chunkIdList 待合并的分片ID列表
     */
    @Override
    public void mergeChunks(String fileId, String chunkCatalog, List<String> chunkIdList) {
        // 校验参数
        AssertUtil.assertThrow("文件合并分片ID列表为空", CollectionUtil.isEmpty(chunkIdList));
        // 获取文件信息,检查文件存在性
        ResourceFileStorageBO fileInfo = fileService.getFileWithStorage(fileId, LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()), LogicQuery.none(), AssertEnum.FOUND);
        AssertUtil.assertThrow(String.format("文件(%s)正在合并...", fileId), Objects.equals(FileIntegrityEnum.MERGING.getCode(), fileInfo.getIntegrity()));
        AssertUtil.assertThrow(String.format("文件(%s)无已上传分片信息", fileId), StringUtils.isBlank(fileInfo.getChunkInfo()));
        // 获取需合并分片
        List<LocChunkBO> chunks2Merge = this.listChunks(fileInfo, chunkCatalog, chunkIdList);
        AssertUtil.assertThrow(String.format("文件(%s)合并分片异常，请重新获取分片列表或提供正确的分片ID", fileId), CollectionUtil.isEmpty(chunks2Merge));

        // 检查原文件（当合并一个已存在的文件时）
        if (Objects.equals(FileIntegrityEnum.NORMAL.getCode(), fileInfo.getIntegrity())) {
            fileService.getFileData(fileId, AssertEnum.IGNORE);
        }

        // backup
        Integer originFileIntegrity = fileInfo.getIntegrity();
        // 标识当前合并中状态
        fileService.updateFileIntegrity(fileInfo.getPid(), FileIntegrityEnum.MERGING.getCode());

        // 开启事务
        DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
        transDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        TransactionStatus transaction = transactionManager.getTransaction(transDef);

        try {
            List<LocChunkStorage> locChunkStorages = MapperUtil.nf().mapAsList(chunks2Merge, LocChunkStorage.class);
            FileStorage fileStorage = this.convert(fileInfo);
            String chunkSupPath = catalogService.getCacheSupPath(chunkCatalog, fileId);
            File fileData = chunkStorageService.mergeChunks(fileStorage, chunkSupPath, locChunkStorages, true, ConflictModeEnum.COVER_TAR);
            // 文件合并完成，更新完整性状态和分片信息
            chunks2Merge.forEach(fileChunkBO -> fileChunkBO.setStatus(LocChunkStatusEnum.MERGED.code()));
            String chunksInfo = HBoxUtil.encodeChunkInfo(chunks2Merge);
            this.updateChunksInfo(fileId, FileIntegrityEnum.NORMAL.getCode(), chunksInfo);
            transactionManager.commit(transaction);
        } catch (Exception e) {
            transactionManager.rollback(transaction);
            // 合并失败，还原文件完整性状态
            fileService.updateFileIntegrity(fileId, originFileIntegrity);
            throw e;
        }
        fileService.dealStorageMode(fileId, StorageModeEnum.SAVE_IN_CLOUD.code(), true);
    }

    @Override
    public void deleteChunks(String fileId, String chunkCatalog) {
        String chunkSupPath = catalogService.getCacheSupPath(chunkCatalog, fileId);
        chunkStorageService.deleteChunks(chunkSupPath);
    }

    /**
     * 文件分片保存参数校验
     */
    private void checkSaveChunkParam(LocChunkBO chunk, byte[] chunkData) {
        // 校验不可为空
        AssertUtil.assertThrow("文件ID为空", StringUtils.isBlank(chunk.getFileId()));
        // 校验分片
        HBoxUtil.verifyFileChunk(chunk, chunkData);
        // 设置默认值
        chunk.setChunkId(SnowFlakeUtil.nextIdStr());
        chunk.setStatus(LocChunkStatusEnum.MERGING.code());
    }

    /**
     * 获取文件分片列表
     * 可选获取指定分片id列
     * 获取过程中会进行分片检查
     */
    private List<LocChunkBO> listChunks(ResourceFileStorageBO fileBO, String chunkCatalog, List<String> chunkIdList) {
        if (Objects.isNull(fileBO) || StrUtil.isBlank(fileBO.getChunkInfo()))
            return new ArrayList<>();
        List<LocChunkBO> chunkList = HBoxUtil.decode(fileBO.getChunkInfo());
        if (CollectionUtil.isNotEmpty(chunkIdList)) {
            chunkList = HBoxUtil.filterChunks(chunkList, chunkIdList);
        }
        // 文件长度
        long fileLength = fileStorageService.getLength(catalogService.getFilePath(fileBO.getCatalog(), fileBO.getResourceKey()));
        // 原分片数量
        int originChunkCount = chunkList.size();
        // 删除未通过校验的分片（如分片丢失，长度信息不一致等）
        chunkList.removeIf(chunk -> !this.existsChunk(chunk, chunkCatalog, fileLength));
        // 若分片有变化，更新
        if (chunkList.size() != originChunkCount) {
            ResourceFileUpdateQuery updateQuery = new ResourceFileUpdateQuery();
            updateQuery.setPid(fileBO.getPid());
            updateQuery.setChunkInfo(HBoxUtil.encodeChunkInfo(chunkList));
            fileService.updateFile(updateQuery);
        }
        return chunkList;
    }

    /**
     * 校验分片是否存在
     */
    private boolean existsChunk(LocChunkBO chunk, String chunkCatalog, long fileLength) {
        if (Objects.equals(chunk.getStatus(), LocChunkStatusEnum.MERGING.code())) {
            // 校验新增分片
            String chunkSupPath = catalogService.getCacheSupPath(chunkCatalog, chunk.getFileId());
            // md5
            if (StrUtil.isNotBlank(chunk.getDigest())) {
                String chunkDigest = chunkStorageService.getDigest(chunkSupPath, chunk.getChunkId(), chunk.getDigestType());
                return Objects.equals(chunkDigest, chunk.getDigest());
            }
            if (Objects.nonNull(chunk.getEndBytes()) && Objects.nonNull(chunk.getStartBytes())) {
                long expectedLength = chunk.getEndBytes() - chunk.getStartBytes() + 1;
                long actualLength = chunkStorageService.getLength(chunkSupPath, chunk.getChunkId());
                return Objects.equals(expectedLength, actualLength);
            }
            return chunkStorageService.exist(chunkSupPath, chunk.getChunkId());
        }
        if (Objects.equals(chunk.getStatus(), LocChunkStatusEnum.MERGED.code())) {
            // 校验已合并分片
            return chunk.getEndBytes() <= fileLength;
        }
        return false;
    }

    private void updateChunksInfo(String fileId, Integer fileIntegrity, String chunksInfo) {
        ResourceFileUpdateQuery updateQuery = new ResourceFileUpdateQuery();
        updateQuery.setPid(fileId);
        updateQuery.setChunkInfo(chunksInfo);
        updateQuery.setIntegrity(fileIntegrity);
        fileService.updateFile(updateQuery);
    }

    private FileStorage convert(ResourceFileStorageBO fileInfo) {
        FileStorage fileStorage = new FileStorage();
        String fileSupPath = catalogService.getFileSupPath(fileInfo.getCatalog(), fileInfo.getResourceKey());
        fileStorage.setFileName(fileInfo.getResourceKey())
            .setSuperPath(fileSupPath)
            .setLength(fileInfo.getLength())
            .setDigest(fileInfo.getStrongChecksum())
            .setDigestType(DigestAlgorithm.MD5.getValue());
        return fileStorage;
    }
}
